package com.crm.miaohe.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.NonNull;
import org.springframework.security.core.AuthenticationException;
import org.springframework.transaction.TransactionException;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.stream.Collectors;

import static com.crm.miaohe.common.ApplicationConstant.VALIDATION_MESSAGE_PATTERN;

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    private final MessageSource messageSource;

    private static final String AUTH_DEFAULT_CODE = "AUTH000";

    private static final String AUTH_DEFAULT_MESSAGE = "Authentication has failed ";

    private static final String COMMON_EX_CODE = "CM000";

    private static final String METHOD_NOT_SUPPORTED = "CM001";
    private static final String REQUEST_FORMAT_ERROR = "CM002";
    private static final String NO_HANDLER_FOUND = "CM003";

    private static final String DB_OPERATION_EX = "DB000";

    public ApiExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorResponse error = produceErrorResponse(METHOD_NOT_SUPPORTED);
        return handleExceptionInternal(ex, error, headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorResponse error = produceErrorResponse(NO_HANDLER_FOUND, ex.getRequestURL());
        return handleExceptionInternal(ex, error, headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        var errors = ex.getBindingResult().getFieldErrors().stream().map(this::produceBindErrorResponse).collect(Collectors.toList());
        return handleExceptionInternal(ex, errors, headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        var errors = ex.getBindingResult().getFieldErrors().stream().map(this::produceBindErrorResponse).collect(Collectors.toList());
        return handleExceptionInternal(ex, errors, headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorResponse error = produceErrorResponse(REQUEST_FORMAT_ERROR);
        return handleExceptionInternal(ex, error, headers, status, request);
    }

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex, WebRequest request) {
        String code = ex.getMessage();
        String message = messageSource.getMessage(ex.getMessage(), null, AUTH_DEFAULT_MESSAGE, Locale.getDefault());
        if (ObjectUtils.isEmpty(code)) {
            code = AUTH_DEFAULT_CODE;
        }
        return handleExceptionInternal(ex, ErrorResponse.builder().code(code).message(message).build(),
                HttpHeaders.EMPTY, HttpStatus.UNAUTHORIZED, request);
    }

    @ExceptionHandler({TransactionException.class, DataAccessException.class, SQLException.class})
    public ResponseEntity<Object> handlePersistentException(Exception ex, WebRequest request) {
        String message = messageSource.getMessage(DB_OPERATION_EX, null, Locale.getDefault());
        return handleExceptionInternal(ex, ErrorResponse.builder().code(DB_OPERATION_EX).message(message).build(),
                HttpHeaders.EMPTY, HttpStatus.INTERNAL_SERVER_ERROR, request);
    }

    @ExceptionHandler(ErpBusinessException.class)
    public ResponseEntity<Object> handleBusinessException(ErpBusinessException e, WebRequest request) {
        var error = produceErrorResponse(e);
        return handleExceptionInternal(e, error, HttpHeaders.EMPTY, e.getStatus(), request);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleCommonException(Exception ex, WebRequest request) {
        String message = messageSource.getMessage(COMMON_EX_CODE, null, "", Locale.getDefault());
        return handleExceptionInternal(ex, ErrorResponse.builder().code(COMMON_EX_CODE).message(message).build(),
                HttpHeaders.EMPTY, HttpStatus.INTERNAL_SERVER_ERROR, request);
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(@NonNull Exception ex, Object body, HttpHeaders headers,
                                                             @NonNull HttpStatus status,
                                                             @NonNull WebRequest request) {
        CommonErrorResponse response = new CommonErrorResponse();
        response.setPath(((ServletWebRequest)request).getRequest().getRequestURI());
        response.setTimestamp(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
        response.setStatus(status.value());
        if (status.is5xxServerError()) {
            log.error("Server has encountered an issue. {}", ex, ex);
        } else if (log.isDebugEnabled()) {
            log.debug(ex.toString(), ex);
        }
        if (Objects.isNull(body)) {
            try {
                String message = messageSource.getMessage(ex.getClass().getName(), null, Locale.getDefault());
                response.setErrors(Collections.singletonList(ErrorResponse.builder()
                        .code(status.toString()).message(message).build()));
            } catch (NoSuchMessageException e) {
                String code = "HttpStatus_" + status.value();
                String message = messageSource.getMessage(code, null, null, Locale.getDefault());
                if (Objects.isNull(message)) {
                    log.warn("The exception has no message.", ex);
                    message = status.getReasonPhrase();
                }
                response.setErrors(Collections.singletonList(ErrorResponse.builder()
                        .code(code).message(message).build()));
            }
        } else if (body instanceof List) {
            response.setErrors((List<?>) body);
        } else if (body instanceof ErrorResponse) {
            response.setErrors(Collections.singletonList((ErrorResponse) body));
        }
        return super.handleExceptionInternal(ex, response, headers, status, request);
    }

    private ErrorResponse produceBindErrorResponse(@NonNull FieldError error) {
        try {
            String pattern = messageSource.getMessage(error, Locale.getDefault());
            Matcher matcher = VALIDATION_MESSAGE_PATTERN.matcher(pattern);
            if (matcher.find()){
                String code = matcher.group("code");
                String message = matcher.group("message");
                return new ErrorResponse(code, message);
            }
        } catch (NoSuchMessageException ex) {
            log.warn("Error code and error message not found. Invalid param: {}", error.getRejectedValue());
            log.error(error.getDefaultMessage());
        }
        return new ErrorResponse(error.getCode(), error.getDefaultMessage());
    }

    private ErrorResponse produceErrorResponse(String code, Object... args) {
        String message = "";
        try {
            message = messageSource.getMessage(code, args, Locale.getDefault());
        } catch (NoSuchMessageException e) {
            log.error("Current code [{}] has no message.", code);
        }
        return new ErrorResponse(code, message);
    }

    private ErrorResponse produceErrorResponse(ErpBusinessException ex) {
        String message = "";
        try {
            message = messageSource.getMessage(ex.getCode(), ex.getMessageParams(), Locale.getDefault());
        } catch (NoSuchMessageException e) {
            log.warn("Code [{}] has no error message.", ex.getCode());
        }
        return new ErrorResponse(ex.getCode(), message);
    }

}
